import nodemon from "nodemon";
import { errors } from "./errors";
import * as fs from "fs";
import * as path from "path";
import * as babel from "@babel/core";
import { ImportPathRewrite } from "./import-path-rewrite";
import watch from "node-watch";

const tsOutExt = ".js";

type FileAction = (filePath: string, outDir: string) => void;

export class ProjectCompiler {

    private fileActions: { [ext: string]: FileAction };
    private skipFiles = ["\\S+\\.d\\.tsx?$", "\\S+\\.test\\.tsx?$", "\\S+\\.spec\\.tsx?$"];

    static tsOutExt = tsOutExt;

    constructor(private projectPath: string, private sourceDirectoryNanme: string, private outputDirectoryName: string) {
        if (!projectPath) throw errors.argumentNull("projectPath");
        if (!sourceDirectoryNanme) throw errors.argumentNull("sourceDirectoryNanme");
        if (!outputDirectoryName) throw errors.argumentNull("outputDirectoryName");

        if (!path.isAbsolute(projectPath))
            throw errors.notAbsolutePath(projectPath);

        let sourcePath = path.join(projectPath, sourceDirectoryNanme);
        if (!fs.existsSync(sourcePath))
            throw errors.pathNotExists(sourcePath);

        this.fileActions = {
            ".ts": this.compileFile.bind(this),
            ".tsx": this.compileFile.bind(this),
        }

    }

    /** 
      * 编译特定文件，并生成到制定目录 
      * @param sourcePath 源文件路径
      * @param outDir 输出目录
      * */
    async compileFile(filePath: string, outDir: string) {
        this.compileTypeScriptFileByBabel(filePath, outDir);
    }

    private async compileTypeScriptFileByBabel(sourcePath: string, outDir: string) {
        if (!sourcePath) throw errors.argumentNull("sourcePath");
        if (!outDir) throw errors.argumentNull("outDir");
        if (!fs.existsSync(sourcePath)) throw errors.pathNotExists(sourcePath);

        let sourceDir = path.dirname(sourcePath);
        let babelConfig = ProjectCompiler.getBabelConfig(this.projectPath, sourceDir);
        let babelOptions = babelConfig.options;
        let babelPath = babelConfig.path;
        babelOptions.filename = sourcePath;
        babelOptions.code = false;
        babelOptions.ast = true;

        let fileResult = babel.transformFileSync(sourcePath, babelOptions);
        if (!fileResult)
            throw errors.compileError(sourcePath);

        let ast = fileResult.ast;
        if (!ast)
            throw errors.compileError(sourcePath);

        new ImportPathRewrite(sourcePath, ast, tsOutExt);
        let r = babel.transformFromAstSync(ast, undefined, {
            filename: sourcePath, plugins: babelOptions.plugins,
            presets: babelOptions.presets, sourceMaps: true
        });
        if (!r || r.code == null)
            throw errors.compileError(sourcePath);

        let ext = path.extname(sourcePath);
        let outExt = this.fileOutExt(sourcePath);
        let targetPath = path.join(outDir, path.basename(sourcePath).replace(ext, outExt));
        let outDirPath = path.resolve(targetPath, "..");

        if (r.map) {
            r.map.file = path.basename(targetPath);
            let sources = r.map.sources || [];
            let sourceDir = path.dirname(sourcePath);
            sources.forEach((s, i) => {
                sources[i] = path.relative(outDirPath, path.join(sourceDir, s));
            });
            r.map.sources = sources;
            let mapPath = targetPath + ".map";
            if (!fs.existsSync(outDirPath))
                fs.mkdirSync(outDirPath, { recursive: true });

            fs.writeFileSync(mapPath, JSON.stringify(r.map));
            r.code += `\n//babelPath:${babelPath}`;
            r.code += "\n//# sourceMappingURL=" + path.basename(sourcePath);
        }

        if (!fs.existsSync(outDirPath))
            fs.mkdirSync(outDirPath, { recursive: true });

        fs.writeFileSync(targetPath, r.code);

    }

    static loadBabelConfig(configPath: string): babel.TransformOptions {
        if (!configPath) throw errors.argumentNull("configPath");
        if (!fs.existsSync(configPath)) throw errors.pathNotExists(configPath);

        let ext = path.extname(configPath);
        if (ext === ".json") {
            let content = fs.readFileSync(configPath, "utf-8");
            return JSON.parse(content);
        }

        if (ext === ".js") {
            let module = require(configPath);
            let config = Object.assign({}, module.default || module);
            return config;
        }

        throw errors.invalidBabelConfigExtension(ext, configPath);
    }

    static findBabelConfigPath(projectPath: string, directoryPath: string): string | null {
        if (!path.isAbsolute(directoryPath)) {
            directoryPath = path.join(projectPath, directoryPath);
        }

        console.assert(path.isAbsolute(projectPath), `projectPath ${projectPath} is not absolute`);
        console.assert(path.isAbsolute(directoryPath), `directoryPath ${directoryPath} is not absolute`);
        let jsonConfigPath = path.join(directoryPath, "babel.config.json");
        let jsConfigPath = path.join(directoryPath, "babel.config.js");
        if (fs.existsSync(jsConfigPath)) {
            return jsConfigPath;
        }
        if (fs.existsSync(jsonConfigPath)) {
            return jsonConfigPath;
        }

        if (projectPath == directoryPath)
            return null;

        let parentPath = path.resolve(directoryPath, "..");

        return this.findBabelConfigPath(projectPath, parentPath);
    }

    static getBabelConfig(projectPath: string, directoryPath: string): { options: babel.TransformOptions, path: string } {
        let configPath = this.findBabelConfigPath(projectPath, directoryPath);
        let babelOptions: babel.TransformOptions;
        if (configPath) {
            babelOptions = this.loadBabelConfig(configPath);
        }
        else {

            babelOptions = this.getDefaultBabelConfig();
        }

        babelOptions.ast = true;
        babelOptions.code = false;

        return { options: babelOptions, path: configPath || '' };

    }

    static getDefaultBabelConfig(): babel.TransformOptions {
        let plugins = [
            "babel-plugin-transform-typescript-metadata",
            ["@babel/plugin-proposal-decorators", { legacy: true }],
            ["@babel/plugin-transform-typescript", { isTSX: true }],
            ["@babel/plugin-transform-react-jsx", { "pragma": "React.createElement", "pragmaFrag": "React.Fragment" }],
        ];
        let presets = [
            ['@babel/preset-env', { targets: { node: 'current' } }],
        ];

        let babelOptions = { plugins, presets };
        return babelOptions;
    }

    /** 
     * 获取源文件所对应生成文件的扩展名 
     * @param file 源文件名
     * */
    private fileOutExt(file: string) {
        let ext = path.extname(file);
        if (ext === ".ts")
            return tsOutExt;

        if (ext === ".tsx")
            return tsOutExt;

        return ext;
    }

    private isIgnoredFile(filePath: string) {
        if (!filePath) throw errors.argumentNull("filePath");

        let isSkip = this.skipFiles.some(pattern => new RegExp(pattern).test(filePath));
        return isSkip;
    }

    generateCode(): void;
    generateCode(sourceDir: string, outDir: string): void;
    generateCode(sourceDir?: string, outDir?: string): void {

        // if (!sourceDir) throw errors.argumentNull("sourceDir");
        // if (!outDir) throw errors.argumentNull("outDir");
        if (!sourceDir)
            sourceDir = path.join(this.projectPath, this.sourceDirectoryNanme);

        if (!outDir)
            outDir = path.join(this.projectPath, this.outputDirectoryName);

        let fileActions = this.fileActions;

        if (!fs.existsSync(sourceDir))
            throw errors.pathNotExists(sourceDir);

        let files = fs.readdirSync(sourceDir);
        for (let file of files) {
            let filePath = path.join(sourceDir, file);
            if (!fs.statSync(filePath).isFile()) {
                continue;
            }

            let isSkip = this.isIgnoredFile(filePath);//skipFiles.some(pattern => new RegExp(pattern).test(filePath));
            if (isSkip) {
                console.log(`Skip ${filePath}`);
                continue;
            }

            let ext = path.extname(file);
            let action = fileActions[ext];
            if (action) {
                action(filePath, outDir);
            }
        }

        let dirs = fs.readdirSync(sourceDir);
        for (let dir of dirs) {
            let fullPath = path.join(sourceDir, dir);
            let outDirPath = path.join(outDir, dir);
            if (fs.statSync(fullPath).isDirectory()) {
                this.generateCode(fullPath, outDirPath);
            }
        }
    }

    watchDirectory() {//sourceRoot: string, outRoot: string
        watch(this.sourceDirectoryNanme, { recursive: true }, async (evt, name) => {
            let action = this.fileActions[path.extname(name)];
            let outPath = path.dirname(name).replace(this.sourceDirectoryNanme, this.outputDirectoryName);
            if (action) {
                action(name, outPath);
            }
        })
    }


    run() {
        nodemon({
            script: `./${this.outputDirectoryName}/main.js`,
            watch: [`./${this.outputDirectoryName}/`],
        })
    }
}